/* Copyright (c) 2010 OFXKit
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#import "OFXDocumentResponse.h"
#import "OFXResponseParser.h"
#import "RegexKitLite.h"
#import "AQXMLParser.h"

@implementation OFXDocumentResponse

@synthesize accountInfo;
@synthesize signOnResponse;
@synthesize statements;
@synthesize securitiesList;

- (id) init
{
  self = [super init];
  if (self != nil)
  {
    [self setStatements:[[NSMutableArray alloc] init]];
  }
  
  return self;
}

+ (id) ofxDocumentWithContentsOfFile:(NSString*)path
                            encoding:(NSStringEncoding)enc 
                               error:(NSError**)error
{
  // load contents of file
  NSString* ofxFile = [NSString stringWithContentsOfFile:path 
                                                encoding:enc
                                                   error:error];
  
  return [OFXDocumentResponse ofxDocumentWithContentsOfString:ofxFile 
                                                     encoding:enc 
                                                        error:error];
}

+ (id) ofxDocumentWithContentsOfURL:(NSURL*)url 
                           encoding:(NSStringEncoding)enc 
                              error:(NSError**)error
{
  return nil;
}

+ (NSString *) convertSgmlToXmlWithString:(NSString *)sgmlString 
{
  // Formerly we were using OpenSP to convert the SGML to valid XML
  // This approach worked well enough; however, it was inconvenient
  // because it required us to maintain a frameworkized version of 
  // OpenSP, spawn an additional process, and prevented us from working 
  // with iOS.
  
  // This approach is not as clean as it will fail for some valid SGML
  // documents that use SGML features that are not valid in XML.  Of
  // all the OFX documents we have seen in the wild though the main
  // incompatibility is that the SGML does not have the closing tags
  // that XML requires.
  
  // This function searches for those tags that are missing closing tags
  // and inserts them into the stream before the next opening tag.
  
  // Possible improvement:
  
  // Mobile devices are often memory constrained.  This function makes an
  // in memory copy of the sgmlString.  We could be more efficient by using
  // insertString:atIndex:, however we would need to investigate when 
  // sgmlString gets released.  At the moment, it appears that either way we 
  // create a copy of sgmlString in memory.
  
  // guestimate of how large the new string will be.
  NSUInteger capacity = [sgmlString length] * 1.1;
  NSMutableString* xmlString = [[NSMutableString alloc] 
                                initWithCapacity:capacity];
  
  // This algorithm searches for SGML tags and then looks for closing tags
  // if no closing tag exists one is inserted immediately before the next
  // opening tag.
  
  // This will fail if there are some segments that contain closing tags
  // while others fail.
  
  // Create an index of closing tags for searching
  NSArray* closingTagArray = [sgmlString componentsMatchedByRegex:@"</[_:A-Za-z][-._:A-Za-z0-9]*\\s*>"];
  NSSet* closingTagIndex = [[NSSet alloc] initWithArray:closingTagArray];
  
  NSRange range; 
  NSRange searchRange = NSMakeRange(0UL, [sgmlString length]);
  NSRange appendRange = NSMakeRange(0UL, NSNotFound);
  
  NSString* closingTag;
  NSString* openTagRegex = @"<[_:A-Za-z][-._:A-Za-z0-9]*\\s*>";
  NSString* openCloseTagRegex = @"<([_:A-Za-z][-._:A-Za-z0-9]*|/[_:A-Za-z][-._:A-Za-z0-9]*)\\s*>";
  range = [sgmlString rangeOfRegex:openTagRegex];
  while (range.location != NSNotFound) {
    // chop off the <> characters from tag
    range = NSMakeRange(range.location + 1, range.length - 2);
    closingTag = [NSString stringWithFormat:@"</%@>", 
                  [sgmlString substringWithRange:range]];
  
    // find location of next opening tag (opening or closing)
    searchRange.location = range.location + range.length + 1;
    searchRange.length = [sgmlString length] - searchRange.location;
    range = [sgmlString rangeOfRegex:openCloseTagRegex inRange:searchRange];
  
    // append characters and check if we need to add closing tag
    appendRange.length = range.location - appendRange.location;
    if([closingTagIndex containsObject:closingTag] == NO) {
      [xmlString appendFormat:@"%@%@", 
       [sgmlString substringWithRange:appendRange], closingTag];
    } else {
      [xmlString appendString:[sgmlString substringWithRange:appendRange]];
}

    appendRange.location = range.location;
    range = [sgmlString rangeOfRegex:openTagRegex inRange:searchRange];
}

  // append last little bit
  appendRange.length = [sgmlString length] - appendRange.location;
  [xmlString appendString:[sgmlString substringWithRange:appendRange]];
  
  return xmlString;
}

+ (id) ofxDocumentWithContentsOfString:(NSString *)ofxString 
                              encoding:(NSStringEncoding)enc
                                 error:(NSError **)error
{
  // detect the version of the header (100 or 200)
  NSString* headerVer = [ofxString stringByMatching:@"OFXHEADER(:|=)\"?(\\d+)"
                                            capture:2L];
  
  OFXDocumentResponse* document = [[OFXDocumentResponse alloc] init];
  [document setHeaderVersion:headerVer];
  
  // parse the document the main differences appear to be a difference
  // in headers.  Changes between versions do not appear to greatly affect
  // parsers... there are just new tags added, etc.  The meaning of tags
  // does not appear to have changed in general.  If this turns out not
  // to be true will want to break the following out into more of a 
  // class heirarchy.
  NSData *ofxData;
  if ([headerVer isEqual:@"100"])
  {
    // document header is version 100, which means it is an SGML header.
    // use regular expressions to parse.
    
    // Parse values we care about.
    NSString* ofxVersion  = [ofxString stringByMatching:@"VERSION:(\\d+)" capture:1L];
    NSString* security    = [ofxString stringByMatching:@"SECURITY:(\\w+)" capture:1L];
    NSString* encoding    = [ofxString stringByMatching:@"ENCODING:(\\w+)" capture:1L];
    NSString* charset     = [ofxString stringByMatching:@"CHARSET:(\\d+)" capture:1L];
    NSString* compression = [ofxString stringByMatching:@"COMPRESSION:(\\w+)" capture:1L];
    NSString* oldFileUID  = [ofxString stringByMatching:@"OLDFILEUID:(\\w+)" capture:1L];
    NSString* newFileUID  = [ofxString stringByMatching:@"NEWFILEUID:(\\w+)" capture:1L];
    
    [document setOfxVersion:ofxVersion];
    [document setSecurity:security];
    [document setEncoding:encoding];
    [document setCharset:charset];
    [document setCompression:compression];
    [document setFileUIDOld:oldFileUID];
    [document setFileUIDNew:newFileUID];
    
    // find location of <OFX> tag and strip everything above off.
    NSRange ofxKeyRange = [ofxString rangeOfString:@"<OFX>"];
    ofxString = [ofxString substringFromIndex:ofxKeyRange.location];
    
    // need to set DTD for conversion of SGML to XML
    NSString *dtdName = [NSString stringWithFormat:@"%@/ofxmain.dtd", ofxVersion];
    NSString *dtdPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"DTDs" ofType:nil];  
    dtdPath = [NSString stringWithFormat:@"%@/%@", dtdPath, dtdName];
    ofxString = [OFXDocumentResponse convertSgmlToXmlWithString:ofxString];
    ofxString = [NSString stringWithFormat:@"<!DOCTYPE OFX PUBLIC \"-//OFX//DTD OFX%@//EN\" \"%@\">\n%@", 
                 ofxVersion, dtdPath, ofxString];
  } else if([headerVer isEqual:@"200"]) {
    // no special header, the document should be a valid XML document
    // just use the standard parser to parse.
  } else {
    NSMutableDictionary* userInfo = [[NSMutableDictionary alloc] initWithCapacity:1];
    [userInfo setObject:@"Unrecognized OFX header version." 
                 forKey:NSLocalizedDescriptionKey];
    *error = [NSError errorWithDomain:@"OFX" code:100 userInfo:userInfo];
    return nil;
  }
  
  // parse the XML
  ofxData = [ofxString dataUsingEncoding:enc];
  AQXMLParser* parser = [[AQXMLParser alloc] initWithData:ofxData];
  OFXResponseParser* ofxParser = [[OFXResponseParser alloc] initWithDocument:document];
  [parser setDelegate:ofxParser];
  [parser parse];
  
  return document;
}

@end
